testomatio-reporter-cli 2.12.0 → 2.14.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/adapter/playwright.js +7 -6
- package/src/bin/cli.js +2 -1
- package/src/pipe/bitbucket.js +31 -29
- package/src/pipe/github.js +32 -30
- package/src/pipe/gitlab.js +29 -28
- package/src/pipe/testomatio.js +3 -0
- package/src/utils/pipe_utils.js +79 -0
- package/types/types.d.ts +3 -0
package/package.json
CHANGED
|
@@ -255,7 +255,8 @@ function appendStep(step, shift = 0) {
|
|
|
255
255
|
newCategory = 'hook';
|
|
256
256
|
break;
|
|
257
257
|
case 'attach':
|
|
258
|
-
|
|
258
|
+
case 'test.attach':
|
|
259
|
+
return null; // Attachments are reported as artifacts, not standalone steps
|
|
259
260
|
default:
|
|
260
261
|
newCategory = 'framework';
|
|
261
262
|
}
|
|
@@ -286,11 +287,11 @@ function appendStep(step, shift = 0) {
|
|
|
286
287
|
resultStep.log = truncate(String(step.log), 250);
|
|
287
288
|
}
|
|
288
289
|
|
|
289
|
-
//
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
);
|
|
290
|
+
// Playwright also associates automatic failure screenshots with the active
|
|
291
|
+
// hook. Only attachments created for an explicit test.step belong to a step;
|
|
292
|
+
// hook and fixture attachments remain test-level result artifacts.
|
|
293
|
+
if (step.category === 'test.step' && step.attachments?.length && SCREENSHOTS_ON_STEPS) {
|
|
294
|
+
const screenshotAttachment = step.attachments.find(isScreenshotArtifact);
|
|
294
295
|
if (screenshotAttachment && screenshotAttachment.path) {
|
|
295
296
|
const artifacts = { screenshot: screenshotAttachment.path };
|
|
296
297
|
addArtifactsToStep(resultStep, artifacts);
|
package/src/bin/cli.js
CHANGED
|
@@ -62,7 +62,8 @@ program
|
|
|
62
62
|
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
|
|
63
63
|
const client = new TestomatClient({ apiKey });
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
// nothing is executed yet; the server flips it to running on the first reported test
|
|
66
|
+
const createRunParams = { status: 'scheduled' };
|
|
66
67
|
if (opts.kind) createRunParams.kind = opts.kind;
|
|
67
68
|
|
|
68
69
|
if (opts.filter) {
|
package/src/pipe/bitbucket.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { APP_PREFIX, testomatLogoURL } from '../constants.js';
|
|
2
2
|
import { ansiRegExp, isSameTest, truncate } from '../utils/utils.js';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
statusEmoji,
|
|
5
|
+
fullName,
|
|
6
|
+
plannedTestsLabel,
|
|
7
|
+
markdownTable,
|
|
8
|
+
runSummary,
|
|
9
|
+
totalDuration,
|
|
10
|
+
} from '../utils/pipe_utils.js';
|
|
4
11
|
import { Gaxios } from 'gaxios';
|
|
5
12
|
import pc from 'picocolors';
|
|
6
13
|
import humanizeDuration from 'humanize-duration';
|
|
@@ -103,37 +110,32 @@ export class BitbucketPipe {
|
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
// Create a comment on Bitbucket
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
'passed',
|
|
120
|
-
)} **${passedCount}** passed; **${statusEmoji('skipped')}** ${skippedCount} skipped |
|
|
121
|
-
| **Duration** | 🕐 **${humanizeDuration(
|
|
122
|
-
parseInt(
|
|
123
|
-
this.tests.reduce((a, t) => a + (t.run_time || 0), 0),
|
|
124
|
-
10,
|
|
125
|
-
),
|
|
126
|
-
{
|
|
127
|
-
maxDecimalPoints: 0,
|
|
128
|
-
},
|
|
129
|
-
)}** |
|
|
130
|
-
`;
|
|
113
|
+
// a scheduled run has no results yet: no counters, no duration
|
|
114
|
+
const isPendingRun = runParams.status === 'pending';
|
|
115
|
+
|
|
116
|
+
/** @type {Object<string, string>} */
|
|
117
|
+
const rows = {};
|
|
118
|
+
|
|
119
|
+
if (isPendingRun) {
|
|
120
|
+
if (this.tests.length) rows.Tests = `⚪ ${plannedTestsLabel(this.tests, this.store.runTestsCount)}`;
|
|
121
|
+
} else {
|
|
122
|
+
rows.Tests = `✔️ **${this.tests.length}** tests run`;
|
|
123
|
+
rows.Summary = runSummary(this.tests);
|
|
124
|
+
rows.Duration = `🕐 **${totalDuration(this.tests)}**`;
|
|
125
|
+
}
|
|
131
126
|
|
|
132
127
|
if (this.ENV.BITBUCKET_BRANCH && this.ENV.BITBUCKET_COMMIT) {
|
|
133
|
-
|
|
134
|
-
|
|
128
|
+
const buildNumber = this.ENV.BITBUCKET_BUILD_NUMBER;
|
|
129
|
+
const buildUrl = `https://bitbucket.org/${this.ENV.BITBUCKET_REPO_FULL_NAME}/pipelines/results/${buildNumber}`;
|
|
130
|
+
rows.Job = `👷 [#${buildNumber}](${buildUrl}) by commit: **${this.ENV.BITBUCKET_COMMIT}**`;
|
|
135
131
|
}
|
|
136
132
|
|
|
133
|
+
const header = [
|
|
134
|
+
``,
|
|
135
|
+
`${statusEmoji(runParams.status)} ${runParams.status.toUpperCase()} ${statusEmoji(runParams.status)}`,
|
|
136
|
+
];
|
|
137
|
+
const summary = `${this.hiddenCommentData}\n\n${markdownTable(header, rows, { boldLabels: true })}`;
|
|
138
|
+
|
|
137
139
|
const failures = this.tests
|
|
138
140
|
.filter(t => t.status === 'failed')
|
|
139
141
|
.slice(0, 20)
|
|
@@ -182,7 +184,7 @@ export class BitbucketPipe {
|
|
|
182
184
|
}
|
|
183
185
|
}
|
|
184
186
|
|
|
185
|
-
if (this.tests.length > 0) {
|
|
187
|
+
if (this.tests.length > 0 && !isPendingRun) {
|
|
186
188
|
body += `\n\n**🐢 Slowest Tests**\n\n`;
|
|
187
189
|
body += this.tests
|
|
188
190
|
.sort((a, b) => b.run_time - a.run_time)
|
package/src/pipe/github.js
CHANGED
|
@@ -5,7 +5,14 @@ import humanizeDuration from 'humanize-duration';
|
|
|
5
5
|
import merge from 'lodash.merge';
|
|
6
6
|
import { testomatLogoURL } from '../constants.js';
|
|
7
7
|
import { ansiRegExp, isSameTest, truncate } from '../utils/utils.js';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
statusEmoji,
|
|
10
|
+
fullName,
|
|
11
|
+
plannedTestsLabel,
|
|
12
|
+
markdownTable,
|
|
13
|
+
runSummary,
|
|
14
|
+
totalDuration,
|
|
15
|
+
} from '../utils/pipe_utils.js';
|
|
9
16
|
import { log } from '../utils/log.js';
|
|
10
17
|
|
|
11
18
|
const debug = createDebugMessages('@testomatio/reporter:pipe:github');
|
|
@@ -75,42 +82,37 @@ class GitHubPipe {
|
|
|
75
82
|
if (!(owner || repo)) return;
|
|
76
83
|
|
|
77
84
|
// ... create a comment on GitHub
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
)} **${passedCount}** passed; **${statusEmoji('skipped')}** ${skippedCount} skipped |
|
|
92
|
-
| Duration | 🕐 **${humanizeDuration(
|
|
93
|
-
parseInt(
|
|
94
|
-
this.tests.reduce((a, t) => a + (t.run_time || 0), 0),
|
|
95
|
-
10,
|
|
96
|
-
),
|
|
97
|
-
{
|
|
98
|
-
maxDecimalPoints: 0,
|
|
99
|
-
},
|
|
100
|
-
)}** |`;
|
|
85
|
+
// a scheduled run has no results yet: no counters, no duration
|
|
86
|
+
const isPendingRun = runParams.status === 'pending';
|
|
87
|
+
|
|
88
|
+
/** @type {Object<string, string>} */
|
|
89
|
+
const rows = {};
|
|
90
|
+
|
|
91
|
+
if (isPendingRun) {
|
|
92
|
+
if (this.tests.length) rows.Tests = `⚪ ${plannedTestsLabel(this.tests, this.store.runTestsCount)}`;
|
|
93
|
+
} else {
|
|
94
|
+
rows.Tests = `✔️ **${this.tests.length}** tests run`;
|
|
95
|
+
rows.Summary = runSummary(this.tests);
|
|
96
|
+
rows.Duration = `🕐 **${totalDuration(this.tests)}**`;
|
|
97
|
+
}
|
|
101
98
|
|
|
102
99
|
if (this.store.runUrl) {
|
|
103
|
-
|
|
100
|
+
rows['Testomat.io Report'] = `📊 [Run #${this.store.runId}](${this.store.runUrl})`;
|
|
104
101
|
}
|
|
105
102
|
if (process.env.GITHUB_WORKFLOW) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}/actions/runs/${process.env.GITHUB_RUN_ID}) | `;
|
|
103
|
+
const server = process.env.GITHUB_SERVER_URL || 'https://github.com';
|
|
104
|
+
rows.Job = `🗂️ [${this.jobKey}](${server}/${this.repo}/actions/runs/${process.env.GITHUB_RUN_ID})`;
|
|
109
105
|
}
|
|
110
106
|
if (process.env.RUNNER_OS) {
|
|
111
|
-
|
|
107
|
+
rows['Operating System'] = `🖥️ \`${process.env.RUNNER_OS}\` ${process.env.RUNNER_ARCH || ''}`;
|
|
112
108
|
}
|
|
113
109
|
|
|
110
|
+
const header = [
|
|
111
|
+
`[](https://testomat.io)`,
|
|
112
|
+
`${statusEmoji(runParams.status)} ${`${process.env.GITHUB_JOB} ${runParams.status}`.toUpperCase()}`,
|
|
113
|
+
];
|
|
114
|
+
const summary = `${this.hiddenCommentData}\n\n${markdownTable(header, rows)}`;
|
|
115
|
+
|
|
114
116
|
const failures = this.tests
|
|
115
117
|
.filter(t => t.status === 'failed')
|
|
116
118
|
.slice(0, 20)
|
|
@@ -170,7 +172,7 @@ class GitHubPipe {
|
|
|
170
172
|
body += '\n\n</details>';
|
|
171
173
|
}
|
|
172
174
|
|
|
173
|
-
if (this.tests.length > 0) {
|
|
175
|
+
if (this.tests.length > 0 && !isPendingRun) {
|
|
174
176
|
body += '\n<details>\n<summary><h3>🐢 Slowest Tests</h3></summary>\n\n';
|
|
175
177
|
body += this.tests
|
|
176
178
|
.sort((a, b) => b?.run_time - a?.run_time)
|
package/src/pipe/gitlab.js
CHANGED
|
@@ -6,7 +6,14 @@ import merge from 'lodash.merge';
|
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import { APP_PREFIX, testomatLogoURL } from '../constants.js';
|
|
8
8
|
import { ansiRegExp, isSameTest, truncate } from '../utils/utils.js';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
statusEmoji,
|
|
11
|
+
fullName,
|
|
12
|
+
plannedTestsLabel,
|
|
13
|
+
markdownTable,
|
|
14
|
+
runSummary,
|
|
15
|
+
totalDuration,
|
|
16
|
+
} from '../utils/pipe_utils.js';
|
|
10
17
|
import { log } from '../utils/log.js';
|
|
11
18
|
|
|
12
19
|
const debug = createDebugMessages('@testomatio/reporter:pipe:gitlab');
|
|
@@ -81,37 +88,31 @@ class GitLabPipe {
|
|
|
81
88
|
if (runParams.tests) runParams.tests.forEach(t => this.addTest(t));
|
|
82
89
|
|
|
83
90
|
// ... create a comment on GitLab
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
'passed',
|
|
98
|
-
)} **${passedCount}** passed; **${statusEmoji('skipped')}** ${skippedCount} skipped |
|
|
99
|
-
| Duration | 🕐 **${humanizeDuration(
|
|
100
|
-
parseInt(
|
|
101
|
-
this.tests.reduce((a, t) => a + (t.run_time || 0), 0),
|
|
102
|
-
10,
|
|
103
|
-
),
|
|
104
|
-
{
|
|
105
|
-
maxDecimalPoints: 0,
|
|
106
|
-
},
|
|
107
|
-
)}** |
|
|
108
|
-
`;
|
|
91
|
+
// a scheduled run has no results yet: no counters, no duration
|
|
92
|
+
const isPendingRun = runParams.status === 'pending';
|
|
93
|
+
|
|
94
|
+
/** @type {Object<string, string>} */
|
|
95
|
+
const rows = {};
|
|
96
|
+
|
|
97
|
+
if (isPendingRun) {
|
|
98
|
+
if (this.tests.length) rows.Tests = `⚪ ${plannedTestsLabel(this.tests, this.store.runTestsCount)}`;
|
|
99
|
+
} else {
|
|
100
|
+
rows.Tests = `✔️ **${this.tests.length}** tests run`;
|
|
101
|
+
rows.Summary = runSummary(this.tests);
|
|
102
|
+
rows.Duration = `🕐 **${totalDuration(this.tests)}**`;
|
|
103
|
+
}
|
|
109
104
|
|
|
110
105
|
if (this.ENV.CI_JOB_NAME && this.ENV.CI_JOB_ID) {
|
|
111
106
|
// eslint-disable-next-line max-len
|
|
112
|
-
|
|
107
|
+
rows.Job = `👷 [${this.ENV.CI_JOB_ID}](${this.ENV.CI_JOB_URL})<br>Name: **${this.ENV.CI_JOB_NAME}**<br>Stage: **${this.ENV.CI_JOB_STAGE}**`;
|
|
113
108
|
}
|
|
114
109
|
|
|
110
|
+
const header = [
|
|
111
|
+
`[](https://testomat.io)`,
|
|
112
|
+
`${statusEmoji(runParams.status)} ${runParams.status.toUpperCase()} ${statusEmoji(runParams.status)}`,
|
|
113
|
+
];
|
|
114
|
+
const summary = `${this.hiddenCommentData}\n\n${markdownTable(header, rows)}`;
|
|
115
|
+
|
|
115
116
|
const failures = this.tests
|
|
116
117
|
.filter(t => t.status === 'failed')
|
|
117
118
|
.slice(0, 20)
|
|
@@ -158,7 +159,7 @@ class GitLabPipe {
|
|
|
158
159
|
body += '\n\n</details>';
|
|
159
160
|
}
|
|
160
161
|
|
|
161
|
-
if (this.tests.length > 0) {
|
|
162
|
+
if (this.tests.length > 0 && !isPendingRun) {
|
|
162
163
|
body += '\n<details>\n<summary><h3>🐢 Slowest Tests</h3></summary>\n\n';
|
|
163
164
|
body += this.tests
|
|
164
165
|
.sort((a, b) => b?.run_time - a?.run_time)
|
package/src/pipe/testomatio.js
CHANGED
|
@@ -323,6 +323,7 @@ class TestomatioPipe {
|
|
|
323
323
|
shared_run: this.sharedRun,
|
|
324
324
|
shared_run_timeout: this.sharedRunTimeout,
|
|
325
325
|
kind: params.kind,
|
|
326
|
+
status: params.status,
|
|
326
327
|
configuration,
|
|
327
328
|
description,
|
|
328
329
|
ci,
|
|
@@ -373,6 +374,8 @@ class TestomatioPipe {
|
|
|
373
374
|
this.store.runUrl = this.runUrl;
|
|
374
375
|
this.store.runPublicUrl = this.runPublicUrl;
|
|
375
376
|
this.store.runId = this.runId;
|
|
377
|
+
// only the server knows how many tests a configuration expands to; automated runs report 0
|
|
378
|
+
if (resp.data.tests_count > 0) this.store.runTestsCount = resp.data.tests_count;
|
|
376
379
|
log.info('📊 Report created. Report ID:', this.runId);
|
|
377
380
|
process.env.runId = this.runId;
|
|
378
381
|
debug('Run created', this.runId);
|
package/src/utils/pipe_utils.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import humanizeDuration from 'humanize-duration';
|
|
1
2
|
import { log } from './log.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -229,6 +230,80 @@ function formatFilterListIds(ids, format) {
|
|
|
229
230
|
}
|
|
230
231
|
}
|
|
231
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Summarize a finished run, e.g. `🔴 **1** failed; 🟢 **8** passed; 🟡 **1** skipped`.
|
|
235
|
+
* The failed part is omitted when nothing failed.
|
|
236
|
+
*
|
|
237
|
+
* @param {Array<{status?: string}>} tests
|
|
238
|
+
* @returns {string}
|
|
239
|
+
*/
|
|
240
|
+
function runSummary(tests) {
|
|
241
|
+
const countOf = status => tests.filter(t => t.status === status).length;
|
|
242
|
+
const failedCount = countOf('failed');
|
|
243
|
+
|
|
244
|
+
const parts = [];
|
|
245
|
+
if (failedCount) parts.push(`${statusEmoji('failed')} **${failedCount}** failed`);
|
|
246
|
+
parts.push(`${statusEmoji('passed')} **${countOf('passed')}** passed`);
|
|
247
|
+
parts.push(`${statusEmoji('skipped')} **${countOf('skipped')}** skipped`);
|
|
248
|
+
|
|
249
|
+
return parts.join('; ');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Total run time of the given tests, humanized — e.g. `2 seconds`.
|
|
254
|
+
*
|
|
255
|
+
* @param {Array<{run_time?: number}>} tests
|
|
256
|
+
* @returns {string}
|
|
257
|
+
*/
|
|
258
|
+
function totalDuration(tests) {
|
|
259
|
+
const milliseconds = tests.reduce((total, t) => total + (t.run_time || 0), 0);
|
|
260
|
+
return humanizeDuration(Math.trunc(milliseconds), { maxDecimalPoints: 0 });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Render a two-column markdown table. Rows with an empty value are skipped, so optional rows
|
|
265
|
+
* need no surrounding `if`.
|
|
266
|
+
*
|
|
267
|
+
* @param {string[]} header - The two header cells.
|
|
268
|
+
* @param {Object<string, string>} rows - Label to value, rendered in insertion order.
|
|
269
|
+
* @param {{ boldLabels?: boolean }} [opts] - Set `boldLabels` to wrap every label in `**`.
|
|
270
|
+
* @returns {string} The table, with no trailing newline.
|
|
271
|
+
*/
|
|
272
|
+
function markdownTable(header, rows, opts = {}) {
|
|
273
|
+
const lines = [`| ${header[0]} | ${header[1]} |`, '| --- | --- |'];
|
|
274
|
+
|
|
275
|
+
for (const [label, value] of Object.entries(rows)) {
|
|
276
|
+
if (!value) continue;
|
|
277
|
+
|
|
278
|
+
if (opts.boldLabels) {
|
|
279
|
+
lines.push(`| **${label}** | ${value} |`);
|
|
280
|
+
} else {
|
|
281
|
+
lines.push(`| ${label} | ${value} |`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return lines.join('\n');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Describe the scope of a run that was prepared but not executed yet. Prefers the server's count;
|
|
290
|
+
* without it, falls back to the scoped ids, which mix tests (`T…`) and suites (`S…`).
|
|
291
|
+
*
|
|
292
|
+
* @param {Array<{test_id?: string}>} tests - Prepared tests the run was scoped to.
|
|
293
|
+
* @param {number} [testsCount] - Real number of tests, as reported by Testomat.io.
|
|
294
|
+
* @returns {string} Markdown label, e.g. `**159** tests planned` or `**6** suites planned`.
|
|
295
|
+
*/
|
|
296
|
+
function plannedTestsLabel(tests, testsCount) {
|
|
297
|
+
if (testsCount > 0) return `**${testsCount}** tests planned`;
|
|
298
|
+
|
|
299
|
+
const suitesCount = tests.filter(t => `${t.test_id || ''}`.startsWith('S')).length;
|
|
300
|
+
const knownTestsCount = tests.length - suitesCount;
|
|
301
|
+
|
|
302
|
+
if (!suitesCount) return `**${knownTestsCount}** tests planned`;
|
|
303
|
+
if (!knownTestsCount) return `**${suitesCount}** suites planned`;
|
|
304
|
+
return `**${knownTestsCount}** tests and **${suitesCount}** suites planned`;
|
|
305
|
+
}
|
|
306
|
+
|
|
232
307
|
export {
|
|
233
308
|
updateFilterType,
|
|
234
309
|
parseFilterParams,
|
|
@@ -236,6 +311,10 @@ export {
|
|
|
236
311
|
setS3Credentials,
|
|
237
312
|
statusEmoji,
|
|
238
313
|
fullName,
|
|
314
|
+
markdownTable,
|
|
315
|
+
runSummary,
|
|
316
|
+
totalDuration,
|
|
317
|
+
plannedTestsLabel,
|
|
239
318
|
parsePipeOptions,
|
|
240
319
|
formatFilterListIds,
|
|
241
320
|
getObjectSize,
|
package/types/types.d.ts
CHANGED
|
@@ -346,6 +346,9 @@ export interface CreateRunParams {
|
|
|
346
346
|
/** Run configuration merged into the server-side run configuration. */
|
|
347
347
|
configuration?: Record<string, any>;
|
|
348
348
|
|
|
349
|
+
/** Initial run status. `scheduled` marks a prepared run; the server flips it to running. */
|
|
350
|
+
status?: 'scheduled';
|
|
351
|
+
|
|
349
352
|
/** Override batch upload mode. */
|
|
350
353
|
batchMode?: BatchMode;
|
|
351
354
|
}
|